<template>
    <div :class="['v-tree__container',{'v-tree--grabbable':zoomable}]">
        <svg :ref="svgRef"
            width="100%"
            height="100%">
            <transition>
                <g :ref="gRef"
                    :transform="`translate(${translate.x},${translate.y}) scale(${scale})`">
                    <Link v-for="(link,index) in tree.links"
                        :key="index"
                        :pathFunc="pathFunc"
                        :direction="direction"
                        :linkData="link"
                        :nodeSize="nodeSize"
                        :transitionDuration="transitionDuration" />

                    <Node v-for="node in tree.nodes"
                        v-if="node.data._display"
                        :key="node.data.id"
                        :nodeData="node"
                        :nodeSize="nodeSize"
                        :transitionDuration="transitionDuration"
                        :direction="direction"
                        @handleNodeToggle="handleNodeToggle" />
                </g>
            </transition>
        </svg>
    </div>
</template>

<script>
/**
 * @file tree.vue
 * @author lili39@didichuxing.com
 * @date 2018-08-28 20:48:27
 */

import clone from 'clone';
import { tree, hierarchy, select, event, zoom } from 'd3';

import { uniqueId, assignProperties } from './helper';
import Link from './link.vue';
import Node from './node.vue';
import './tree.less';
export default {
    name: 'Tree',
    provide() {
        return {
            $$tree: this
        };
    },
    components: {
        Link,
        Node
    },
    props: {
        initData: {
            type: Array,
            required: true
        },
        direction: {
            type: String,
            default: 'vertical'
        },
        collapsible: {
            type: Boolean,
            default: true
        },
        deepFactor: {
            type: Number,
            default: 0.65
        },
        pathFunc: {
            default: 'straight'
        },
        nodeSize: {
            type: Object,
            default() {
                return {
                    x: 160,
                    y: 80
                };
            }
        },
        separation: {
            type: Object,
            default() {
                return {
                    siblings: 1.2,
                    nonSiblings: 1.2
                };
            }
        },
        zoomable: {
            type: Boolean,
            default: true
        },
        zoom: 1,
        scaleExtent: {
            type: Object,
            default() {
                return {
                    min: 0.1,
                    max: 1
                };
            }
        },
        translate: {
            type: Object,
            default() {
                return {
                    x: 0,
                    y: 0
                };
            }
        },
        scale: {
            type: Number,
            default: 1
        },
        transitionDuration: {
            type: Number,
            default: 500
        }
    },
    data() {
        return {
            svgRef: uniqueId(),
            gRef: uniqueId(),
            data: assignProperties(this.initData),
            nodes: [],
            links: []
        };
    },
    computed: {
        root() {
            const root = hierarchy(
                this.data[0],
                d => (d._collapsed ? null : d._children)
            );
            return root;
        },
        treemap() {
            const { x, y } = this.nodeSize;
            const treemap = tree()
                .nodeSize(this.direction === 'horizontal' ? [y, x] : [x, y])
                .separation(
                    (a, b) =>
                        a.parent.data.id == b.parent.data.id
                            ? this.separation.siblings
                            : this.separation.nonSiblings
                );
            return treemap;
        },
        tree() {
            const treeData = this.treemap(this.root);
            const nodes = treeData.descendants();
            nodes.forEach(d => {
                d.y = d.depth * 180;
            });
            const links = treeData.descendants().slice(1);
            nodes.forEach(node => {
                this.direction === 'horizontal'
                    ? (node.y = node.y * (1 / this.deepFactor))
                    : (node.y = node.y * this.deepFactor);
            });
            return { nodes, links };
        }
    },
    watch: {
        initData: {
            deep: true,
            handler(val) {
                this.data = assignProperties(clone(val));
            }
        },
        direction() {
            this.data = assignProperties(clone(this.initData));
        }
    },
    methods: {
        bindZoomListener() {
            const svg = select(this.$refs[this.svgRef]);
            const g = select(this.$refs[this.gRef]);

            if (this.zoomable) {
                svg.call(
                    zoom()
                        .scaleExtent([
                            this.scaleExtent.min,
                            this.scaleExtent.max
                        ])
                        .on('zoom', () => {
                            const [lastX, lastY] = [
                                this.translate.x,
                                this.translate.y
                            ];
                            const { x, y, k } = event.transform;
                            g.attr(
                                'transform',
                                `translate(${lastX + x},${lastY +
                                    y}) scale(${k})`
                            );
                        })
                );
            }
        },
        setInitialTreeDepth(nodeSet, initialDepth) {
            nodeSet.forEach(n => {
                n._collapsed = n.depth >= initialDepth;
            });
        },
        handleNodeToggle(node) {
            const data = [...this.data];
            const targetNode = this.findNodeById(node.data.id, data)[0];
            if (this.collapsible) {
                targetNode._collapsed
                    ? this.expandNode(targetNode)
                    : this.collapseNode(targetNode);
                this.data = data;
            }
        },
        expandNode(node) {
            node._collapsed = false;
        },
        collapseNode(node) {
            node._collapsed = true;
            // if (node._children && node._children.length > 0) {
            //     node._children.forEach(child => {
            //         this.collapseNode(child);
            //     });
            // }
        },
        findNodeById(nodeId, nodeSet, hits = []) {
            if (hits.length > 0) {
                return hits;
            }
            hits = hits.concat(nodeSet.filter(node => node.id === nodeId));

            nodeSet.forEach(node => {
                if (node._children && node._children.length > 0) {
                    hits = this.findNodeById(nodeId, node._children, hits);
                    return hits;
                }
                return hits;
            });

            return hits;
        },
        update() {}
    },
    mounted() {
        this.bindZoomListener();
        const { width, height } = this.$refs[
            this.svgRef
        ].getBoundingClientRect();
        this.viewPort = {
            width,
            height
        };
    }
};
</script>

<style lang="less">
.v-tree__container {
    width: 100%;
    height: 100%;
}

.v-tree--grabbable {
    cursor: move;
    cursor: grab;
    &:active {
        cursor: grabbing;
    }
}

.v-tree__link-base {
    fill: none;
    stroke: #d8dce5;
    stroke-width: 1px;
}

.v-tree__node-base {
    &:hover {
        .v-tree__collapse-icon {
            opacity: 1;
            circle {
                transform: scale(1);
            }
        }
    }
}
.v-tree__collapse-icon {
    cursor: pointer;
    // opacity: 0;
    transition: all linear 0.3s;
    circle {
        fill: #d8dce5;
        transform: scale(1);
    }
}
</style>

